package com.frank.oj.schedule;

import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.frank.model.entity.common.File;
import com.frank.model.entity.judge.Judge;
import com.frank.model.entity.msg.AdminSysNotice;
import com.frank.model.entity.msg.UserSysNotice;
import com.frank.model.entity.problem.Problem;
import com.frank.model.entity.user.Session;
import com.frank.model.entity.user.SystemLog;
import com.frank.model.entity.user.UserInfo;
import com.frank.model.entity.user.UserRecord;
import com.frank.oj.constant.Constants;
import com.frank.oj.manager.AdminNoticeManager;
import com.frank.oj.model.entity.RecentContest;
import com.frank.oj.service.admin.rejudge.RejudgeService;
import com.frank.oj.service.admin.system.SystemLogService;
import com.frank.oj.service.entity.common.FileEntityService;
import com.frank.oj.service.entity.judge.JudgeEntityService;
import com.frank.oj.service.entity.msg.AdminSysNoticeEntityService;
import com.frank.oj.service.entity.msg.UserSysNoticeEntityService;
import com.frank.oj.service.entity.problem.ProblemEntityService;
import com.frank.oj.service.entity.user.SessionEntryService;
import com.frank.oj.service.entity.user.UserInfoEntityService;
import com.frank.oj.service.entity.user.UserRecordEntityService;
import com.frank.oj.utils.JsoupUtils;
import com.frank.oj.utils.OJCollectorUtils;
import com.frank.oj.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.time.DateFormatUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author frank
 * @Date 2024/4/19
 */
@Component
@Slf4j
public class ScheduledTask {
    @Resource
    private FileEntityService fileEntityService;

    @Resource
    private RedisUtils redisUtils;

    @Resource
    private UserInfoEntityService userInfoEntityService;

    @Resource
    private UserRecordEntityService userRecordEntityService;

    @Resource
    private SessionEntryService sessionEntityService;

    @Resource
    private AdminSysNoticeEntityService adminSysNoticeEntityService;

    @Resource
    private UserSysNoticeEntityService userSysNoticeEntityService;

    @Resource
    private JudgeEntityService judgeEntityService;

    @Resource
    private RejudgeService rejudgeService;

    @Resource
    private ProblemEntityService problemEntityService;

    @Resource
    private AdminNoticeManager adminNoticeManager;

    @Resource
    private ApplicationContext applicationContext;
    @Resource
    private SystemLogService systemLogService;
    @Resource
    private OJCollectorUtils ojCollectorUtils;

    /**
     * 每天1点删除弃用头像
     */
    @Scheduled(cron = "0 0 1 * * *") // 每天1点
    public void deleteAvatar() {
        LambdaQueryWrapper<File> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(File::getType, "avatar").eq(File::getDelete, 1);
        List<File> files = fileEntityService.list(queryWrapper);
        if (files.isEmpty()) {
            return;
        }
        List<Long> idList = new LinkedList<>();
        for (File file : files) {
            if (file.getDelete()) {
                boolean del = FileUtil.del(file.getFilePath());
                if (del) {
                    idList.add(file.getId());
                }
            }
        }

        boolean removed = fileEntityService.removeByIds(idList);
        if (removed) {
            log.info("HC-Scheduled: 删除【" + idList + "】无用头像-成功");
        } else {
            log.error("HC-Scheduled: 删除【" + idList + "】无用头像-失败");
        }
    }

    /**
     * 每天2点删除测试用例
     */
    @Scheduled(cron = "0 0 2 * * *")
    public void deleteTestCase() {
        boolean del = FileUtil.del(Constants.File.TESTCASE_TMP_FOLDER.getPath());
        if (!del) {
            log.error("HC-Scheduled: 删除测试数据-失败");
        }
    }

    /**
     * 每天3点删除比赛打印文本
     */
    @Scheduled(cron = "0 0 3 * * *")
    public void deleteContestPrintText() {
        boolean del = FileUtil.del(Constants.File.CONTEST_TEXT_PRINT_FOLDER.getPath());
        if (!del) {
            log.error("HC-Scheduled: 删除比赛打印文本-失败");
        }
    }

    /**
     * 每天3点删除用户session
     */
    @Scheduled(cron = "0 0 3 * * *")
    public void deleteUserSession() {
        QueryWrapper<Session> sessionQueryWrapper = new QueryWrapper<>();
        DateTime dateTime = DateUtil.offsetMonth(new Date(), -6);
        String strTime = DateFormatUtils.format(dateTime, "yyyy-MM-dd HH:mm:ss");
        sessionQueryWrapper.select("distinct uid");
        sessionQueryWrapper.apply("UNIX_TIMESTAMP(gmt_create) >= UNIX_TIMESTAMP('" + strTime + "')");

        // 根据查询条件获取会话列表
        List<Session> sessionList = sessionEntityService.list(sessionQueryWrapper);
        if (!sessionList.isEmpty()) {
            List<String> uidList = sessionList.stream().map(Session::getUid).collect(Collectors.toList());
            // 构造删除条件，选择在六个月之前创建的会话
            QueryWrapper<Session> queryWrapper = new QueryWrapper<>();
            queryWrapper.in("uid", uidList).apply("UNIX_TIMESTAMP('" + strTime + "') > UNIX_TIMESTAMP(gmt_create)");
            // 根据删除条件获取需要删除的会话列表
            List<Session> deleteSession = sessionEntityService.list(queryWrapper);
            if (!deleteSession.isEmpty()) {
                // 提取需要删除的会话的IP地址列表
                List<String> list = deleteSession.stream().map(Session::getIp).collect(Collectors.toList());
                // 尝试批量删除这些会话记录
                boolean removed = sessionEntityService.removeByIds(list);
                if (!removed) {
                    // 如果删除失败，记录错误日志
                    log.error("HC-Scheduled: 删除过期用户Session-失败");
                }
            }
        }
    }

    /**
     * 删除一月前的系统日志
     */
    @Scheduled(cron = "0 0 12 ? * FRI")
    public void deleteLog() {
        QueryWrapper<SystemLog> queryWrapper = new QueryWrapper<>();
        DateTime dateTime = DateUtil.offsetMonth(new Date(), -1);
        String strTime = DateFormatUtils.format(dateTime, "yyyy-MM-dd HH:mm:ss");

        queryWrapper.apply("UNIX_TIMESTAMP(gmt_create) >= UNIX_TIMESTAMP('" + strTime + "')");
        boolean remove = systemLogService.remove(queryWrapper);
        if (!remove) {
            log.error("HC-Scheduled: 删除系统日志-失败");
        }
    }

    /**
     * 每小时推送未读的系统通知
     */
    @Scheduled(cron = "0 0 0/1 * * *")
    public void syncNoticeToRecentHalfYearUser() {
        LambdaQueryWrapper<AdminSysNotice> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AdminSysNotice::getState, false);
        List<AdminSysNotice> adminSysNotices = adminSysNoticeEntityService.list(queryWrapper);
        if (adminSysNotices.isEmpty()) {
            return;
        }

        // 半年内登陆过的用户
        QueryWrapper<Session> sessionQueryWrapper = new QueryWrapper<>();
        sessionQueryWrapper.select("DISTINCT uid");
        List<Session> sessionList = sessionEntityService.list(sessionQueryWrapper);
        List<String> userIds = sessionList.stream().map(Session::getUid).collect(Collectors.toList());

        for (AdminSysNotice adminSysNotice : adminSysNotices) {
            switch (adminSysNotice.getType()) {
                case "All":
                    List<UserSysNotice> userSysNotices = new ArrayList<>();
                    for (String userId : userIds) {
                        UserSysNotice userSysNotice = new UserSysNotice();
                        userSysNotice.setRecipientId(userId).setType("Sys").setSysNoticeId(adminSysNotice.getId());

                        userSysNotices.add(userSysNotice);
                    }
                    boolean saved = userSysNoticeEntityService.saveOrUpdateBatch(userSysNotices);
                    if (saved) {
                        adminSysNotice.setState(true);
                    }
                    break;
                case "Single":
                    UserSysNotice userSysNotice = new UserSysNotice();
                    userSysNotice.setRecipientId(adminSysNotice.getRecipientId()).setType("Mine").setSysNoticeId(adminSysNotice.getId());

                    boolean saved1 = userSysNoticeEntityService.saveOrUpdate(userSysNotice);
                    if (saved1) {
                        adminSysNotice.setState(true);
                    }
                case "Admin":
                    break;
            }
        }

        boolean saved = adminSysNoticeEntityService.saveOrUpdateBatch(adminSysNotices);
        if (!saved) {
            log.error("HC-Scheduled: 系统通知推送失败");
        }
    }

    /**
     * 每20分钟检查一次，如果20分钟内没有被判为Pending的提交，则认为判题机挂了，需要重新判
     */
    @Scheduled(cron = "0 0/20 * * * ?")
    public void check20MPendingSubmission() {
        DateTime dateTime = DateUtil.offsetMinute(new Date(), -15);
        String strTime = DateFormatUtils.format(dateTime, "yyyy-MM-dd HH:mm:ss");

        QueryWrapper<Judge> judgeQueryWrapper = new QueryWrapper<>();
        judgeQueryWrapper.select("distinct submit_id");
        judgeQueryWrapper.eq("status", Constants.Judge.STATUS_PENDING.getStatus());
        judgeQueryWrapper.apply("UNIX_TIMESTAMP('" + strTime + "') > UNIX_TIMESTAMP(gmt_modified)");

        List<Judge> judgeList = judgeEntityService.list(judgeQueryWrapper);
        if (!CollectionUtils.isEmpty(judgeList)) {
            log.info("Half An Hour Check Pending Submission to Rejudge:" + Arrays.toString(judgeList.toArray()));
            for (Judge judge : judgeList) {
                rejudgeService.rejudge(judge.getSubmitId());
            }
        }
    }

    @Scheduled(cron = "0 0 6 * * *")
    public void checkUnHandleGroupProblemApplyProgress() {
        LambdaQueryWrapper<Problem> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.likeRight(Problem::getApplyPublicProgress, 1).isNotNull(Problem::getGid);
        int count = problemEntityService.count(queryWrapper);

        if (count > 0) {
            String title = "团队题目审批通知(Group Problem Approval Notice)";
            String content = getDissolutionGroupContent(count);

            List<String> superAdminUidList = userInfoEntityService.getSuperAdminUidList();
            List<String> problemAdminUidList = userInfoEntityService.getProblemAdminUidList();
            if (!CollectionUtils.isEmpty(problemAdminUidList)) {
                superAdminUidList.addAll(problemAdminUidList);
            }

            adminNoticeManager.addSingleNoticeToBatchUser(null, superAdminUidList, title, content, "Sys");
        }
    }

    private String getDissolutionGroupContent(int count) {
        return "您好，尊敬的管理员，目前有【" + count +
                "】条团队题目正在申请公开的单子，请您尽快前往后台 [团队题目审批](/admin/group-problem/apply) 进行审批！"
                + "\n\n" +
                "Hello, dear administrator, there are currently **" + count
                + "** problem problems applying for public list. " +
                "Please go to the backstage [Group Problem Examine](/admin/group-problem/apply) for approval as soon as possible!";
    }

    /**
     * 每2小时获取一次牛客OJ比赛列表
     */
    //@Scheduled(cron = "0 0 0/2 * * *")
    @Deprecated
    public void getOjContestList() {
        String nowcoderContestAPI = "https://ac.nowcoder.com/acm/calendar/contest?token=&month=%d-%d";
        ArrayList<Map<String, Object>> contestList = new ArrayList<>();

        DateTime dateTime = DateUtil.date();
        // 近三个月
        for (int offsetMonth = 0; offsetMonth <= 2; offsetMonth++) {
            DateTime newDate = DateUtil.offsetMonth(dateTime, offsetMonth);
            String contestAPI = String.format(nowcoderContestAPI, newDate.year(), newDate.month() + 1);

            /*{
                "contestId": 1076691,
                    "ojName": "NowCoder",
                    "ojId": 0,
                    "link": "https://ac.nowcoder.com/acm/contest/76691?from=acm_calendar",
                    "startTime": 1711969200000,
                    "endTime": 1711976400000,
                    "contestName": "牛客2024年愚人节比赛"
            },*/
            try {
                JSONObject resultObject = JsoupUtils.getJsonFromConnection(JsoupUtils.getConnectionFromUrl(contestAPI, null, null));
                JSONArray contestsArray = resultObject.getJSONArray("data");

                for (int i = contestsArray.size() - 1; i >= 0; i--) {
                    JSONObject contest = contestsArray.getJSONObject(i);
                    if (contest.getLong("endTime", 0L) < dateTime.getTime()) {
                        break;
                    }
                    contestList.add(MapUtil.builder(new HashMap<String, Object>()).put("oj", contest.getStr("ojName")).put("url", contest.getStr("link")).put("title", contest.getStr("contestName")).put("beginTime", new Date(contest.getLong("startTime"))).put("endTime", new Date(contest.getLong("endTime"))).map());
                }
            } catch (IOException e) {
                log.error("HC-Scheduled:获取OJ比赛列表异常{}", e.getMessage());
            }
        }

        contestList.sort((o1, o2) -> {
            long beginTime1 = ((Date) o1.get("beginTime")).getTime();
            long beginTime2 = ((Date) o2.get("beginTime")).getTime();

            return Long.compare(beginTime1, beginTime2);
        });

        String key = Constants.Schedule.RECENT_OTHER_CONTEST.getCode();
        redisUtils.set(key, contestList, 60 * 60 * 24);
        log.info("HC-Scheduled:共获取牛客网【" + contestList.size() + "】条比赛信息");
    }

    /**
     * 每3小时获取一次OJ比赛列表
     */
    @Scheduled(cron = "0 0 0/3 * * *")
    public void getOjContestList2() {

        try {
            Map<String, Object> contests = ojCollectorUtils.CollectContests();
            String key = Constants.Schedule.RECENT_OTHER_CONTEST.getCode();
            redisUtils.set(key, contests, 60 * 60 * 3);
        } catch (Exception e) {
            log.error("CollectContests Exception" + e.toString());
        }
    }

    @Scheduled(cron = "0 0 3 * * *")
    public void getCodeforcesRating() {
        String codeforcesUserInfoAPI = "https://codeforces.com/api/user.info?handles=%s";
        QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
        userInfoQueryWrapper.isNotNull("cf_username");

        List<UserInfo> userInfoList = userInfoEntityService.list(userInfoQueryWrapper);
        for (UserInfo userInfo : userInfoList) {
            String cfUsername = userInfo.getCfUsername();
            String uuid = userInfo.getUuid();
            String ratingAPI = String.format(codeforcesUserInfoAPI, cfUsername);
            try {
                // 连接api，获取json格式对象
                ScheduledTask service = applicationContext.getBean(ScheduledTask.class);
                JSONObject resultObject = service.getCFUserInfo(ratingAPI);
                // 获取状态码
                String status = resultObject.getStr("status");
                // 如果查无此用户，则跳过
                if ("FAILED".equals(status)) {
                    continue;
                }
                // 用户信息存放在result列表中的第0个
                JSONObject cfUserInfo = resultObject.getJSONArray("result").getJSONObject(0);
                // 获取cf的分数
                Integer cfRating = cfUserInfo.getInt("rating", null);
                UpdateWrapper<UserRecord> userRecordUpdateWrapper = new UpdateWrapper<>();
                // 将对应的cf分数修改
                userRecordUpdateWrapper.eq("uid", uuid).set("rating", cfRating);
                boolean result = userRecordEntityService.update(userRecordUpdateWrapper);
                if (!result) {
                    log.error("插入UserRecord表失败------------------------------->");
                }

            } catch (Exception e) {
                log.error("爬虫爬取Codeforces Rating分数异常----------------------->{}", e.getMessage());
            }
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.info("HC-Scheduled:获取Codeforces Rating信息成功");
    }

    @Retryable(value = Exception.class, maxAttempts = 5, backoff = @Backoff(delay = 1000, multiplier = 1.4))
    private JSONObject getCFUserInfo(String url) throws IOException {
        return JsoupUtils.getJsonFromConnection(JsoupUtils.getConnectionFromUrl(url, null, null));
    }
}
